﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/* BoundActionField.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Threading;

#endregion

#nullable enable

namespace AM;

/// <summary>
/// A field containing a bound action.
/// </summary>
/// <typeparam name="T">The type of context for the action.</typeparam>
public sealed class BoundActionField<T>
{
    private BoundAction? _field;

    /// <summary>
    /// Initializes the field with the specified action and context.
    /// </summary>
    /// <param name="action">The action delegate.</param>
    /// <param name="context">The context.</param>
    public BoundActionField(Action<T> action, T context)
    {
        _field = new BoundAction(action, context);
    }

    /// <summary>
    /// Whether the field is empty.
    /// </summary>
    public bool IsEmpty => Interlocked.CompareExchange(ref _field, null, null) == null;

    /// <summary>
    /// Atomically retrieves the bound action from the field and sets the field to <c>null</c>. May return <c>null</c>.
    /// </summary>
    public IBoundAction? TryGetAndUnset()
    {
        return Interlocked.Exchange(ref _field, null);
    }

    /// <summary>
    /// Attempts to update the context of the bound action stored in the field. Returns <c>false</c> if the field is <c>null</c>.
    /// </summary>
    /// <param name="contextUpdater">The function used to update an existing context. This may be called more than once if more than one thread attempts to simultaneously update the context.</param>
    public bool TryUpdateContext(Func<T, T> contextUpdater)
    {
        _ = contextUpdater ?? throw new ArgumentNullException(nameof(contextUpdater));
        while (true)
        {
            var original = Interlocked.CompareExchange(ref _field, null, null);
            if (original == null)
                return false;
            var updatedContext = new BoundAction(original, contextUpdater);
            var result = Interlocked.CompareExchange(ref _field, updatedContext, original);
            if (ReferenceEquals(original, result))
                return true;
        }
    }

    /// <summary>
    /// An action delegate bound with its context.
    /// </summary>
    public interface IBoundAction
    {
        /// <summary>
        /// Executes the action. This should only be done after the bound action is retrieved from a field by <see cref="TryGetAndUnset"/>.
        /// </summary>
        void Invoke();
    }

    private sealed class BoundAction : IBoundAction
    {
        private readonly Action<T> _action;
        private readonly T _context;

        public BoundAction(Action<T> action, T context)
        {
            _action = action;
            _context = context;
        }

        public BoundAction(BoundAction originalBoundAction, Func<T, T> contextUpdater)
        {
            _action = originalBoundAction._action;
            _context = contextUpdater(originalBoundAction._context);
        }

        public void Invoke() => _action?.Invoke(_context);
    }
}
